/*
 * Copyright (C) 2008 Alessandro Sivieri <alessandro.sivieri@chimera-bellerofonte.eu>.
 *
 *  This file is part of Daex.
 *
 *  Daex is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Foobar is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
 */
package eu.chimera_bellerofonte.tesi;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.print.PrinterException;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;

import javax.swing.ComboBoxModel;
import javax.swing.ImageIcon;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;

import eu.chimera_bellerofonte.tesi.datatypes.Album;
import eu.chimera_bellerofonte.tesi.datatypes.AlbumWriterException;
import eu.chimera_bellerofonte.tesi.datatypes.Dvd;
import eu.chimera_bellerofonte.tesi.datatypes.DvdException;
import eu.chimera_bellerofonte.tesi.datatypes.DvdNative;
import eu.chimera_bellerofonte.tesi.datatypes.Genre;
import eu.chimera_bellerofonte.tesi.datatypes.Song;
import eu.chimera_bellerofonte.tesi.datatypes.DefaultAlbumWriter;
import eu.chimera_bellerofonte.tesi.encoding.DefaultEncoder;
import eu.chimera_bellerofonte.tesi.encoding.DefaultEncoderExecutor;
import eu.chimera_bellerofonte.tesi.encoding.DefaultEncodingList;
import eu.chimera_bellerofonte.tesi.encoding.Encoder;
import eu.chimera_bellerofonte.tesi.encoding.EncoderExecutor;
import eu.chimera_bellerofonte.tesi.encoding.EncodingList;
import eu.chimera_bellerofonte.tesi.encoding.Program;
import eu.chimera_bellerofonte.tesi.encoding.ProgramNotFoundException;
import eu.chimera_bellerofonte.tesi.ui.ConsoleDialog;
import eu.chimera_bellerofonte.tesi.ui.MainFrame;
import eu.chimera_bellerofonte.tesi.ui.ProgressBarListener;
import eu.chimera_bellerofonte.tesi.ui.SettingsDialog;
import eu.chimera_bellerofonte.tesi.ui.TextAreaListener;
import eu.chimera_bellerofonte.tesi.ui.models.DaexComboBoxModel;
import eu.chimera_bellerofonte.tesi.ui.models.DaexProgressTableModel;
import eu.chimera_bellerofonte.tesi.ui.models.DaexTable;
import eu.chimera_bellerofonte.tesi.ui.models.DefaultSortedListModel;
import eu.chimera_bellerofonte.tesi.ui.models.SortedListModel;


/**
 * <p>Classe definita per il management del sistema "a gangli"
 * definito finora; implementa alcuni listener per poter
 * mettere in relazione i sistemi tra di loro.</p>
 * <p>Questa classe e' specifica dell'implementazione che si
 * decide di dare al sistema, ed e' implementata come Singleton
 * in quanto e' un'unica istanza all'interno della singola
 * esecuzione.</p>
 * 
 * @author Sivieri Alessandro
 * @author Vitucci Nicola
 * @version 18/mag/07
 */
public class MainManager implements ActionListener, TableModelListener
{
	/**
	 * Settaggio del valore di default della qualita' per gli encoders.
	 */
	public static final int DEFAULTQUALITY=3;
	/**
	 * Proprieta' indicante la modifica di qualcosa nella base di dati.
	 */
	public static final String MODIFYANY="modify";
	
	private static MainManager mainManager=null;
	private Album album=null;
	private MainFrame mainFrame=null;
	private DaexTable tracksTable=null;
	private ComboBoxModel genreModel=null;
	private Encoder[] encoders=null;
	private SortedListModel[] listModels=null;
	private DaexProgressTableModel progressModel=null;
	private SettingsDialog settingsDialog=null;
	private EncodingList encodingList=null;
	private EncoderExecutor executor=null;
	private SpinnerModel spinModel=null;
	/**
	 * Questa proprieta' e' utilizzata per avvertire dei cambiamenti
	 * della base di dati; viene lanciata utilizzando valori casuali
	 * booleani nel fire.
	 */
	private PropertyChangeSupport property=null;
	
	/**
	 * Metodo Singleton per ottenere l'istanza comune dell'applicativo
	 * per la gestione del sistema.
	 * 
	 * @return l'istanza del manager
	 */
	public static MainManager getMainManager()
	{
		if(mainManager==null)
		{
			DaexLogger.getDaexLogger().info("Creo l'istanza del MainManager");
			mainManager=new MainManager();
		}
		return mainManager;
	}
	
	/**
	 * Costruttore privato per il Singleton, richiama tutti gli
	 * initializer.
	 */
	private MainManager()
	{
		property=new PropertyChangeSupport(this);
		try
		{
			// chiamata al metodo vuoto: se prima esecuzione, il device viene
			// caricato con lo static!
			album=getAlbumFromDevice();
		}
		catch (DvdException e)
		{
			DaexLogger.getDaexLogger().log(Level.WARNING, "Nessun dvd inserito all'avvio", e);
		}
		finally
		{
			try
			{
				initModels(album);
			}
			catch (Exception e)
			{
				DaexLogger.getDaexLogger().log(Level.SEVERE, "Errore in fase di creazione", e);
				System.exit(1);
			}
			initGui();
			show();
		}
	}
	
	/**
	 * Inizializzatore dell'oggetto Album a partire dal device
	 * di default.
	 * 
	 * @return un oggetto Album completo
	 * @throws DvdException eccezione lanciata se si sono presentati dei problemi
	 */
	private Album getAlbumFromDevice() throws DvdException
	{
		Dvd dvd=new DvdNative();
		Album album=new Album(dvd);
		DaexLogger.getDaexLogger().info("Init dell'Album corretta.");
		return album;
	}
	
	/**
	 * Inizializzatore dell'oggetto Album a partire dal device
	 * specificato.
	 * 
	 * @param device il device da cui leggere
	 * @return un oggetto Album completo
	 * @throws DvdException eccezione lanciata se si sono presentati dei problemi
	 */
	private Album getAlbumFromDevice(String device) throws DvdException
	{
		Dvd dvd=new DvdNative(device);
		Album album=new Album(dvd);
		DaexLogger.getDaexLogger().info("Init dell'Album corretta.");
		return album;
	}
	
	/**
	 * Metodo di inizializzazione della GUI a partire dai dati finora
	 * costruiti.
	 */
	private void initGui()
	{
		if(album!=null)
		{
			mainFrame=new MainFrame(listModels, tracksTable, progressModel, genreModel, album.getArtist(), album.getAlbum(), spinModel, ((DvdNative) album.getDvd()).getDevice());
		}
		else
		{
			mainFrame=new MainFrame(listModels, tracksTable, progressModel, genreModel, Album.GENERICARTIST, Album.GENERICALBUM, spinModel, DvdNative.getDefaultDevice());
		}
		mainFrame.addArtistActionListener(this);
		mainFrame.addGenreActionListener(this);
		property.addPropertyChangeListener(MODIFYANY, mainFrame);
	}
	
	/**
	 * Inizializzazione di tutti i modelli necessari alla GUI.
	 * 
	 * @param album l'oggetto album da utilizzare (puo' essere null)
	 * @throws ProgramNotFoundException eccezione lanciata se vi sono problemi nell'init degli encoders
	 * @throws DvdException eccezzione lanciata in caso di problemi con il dvd
	 */
	private void initModels(Album album) throws ProgramNotFoundException, DvdException
	{
		this.genreModel=new DaexComboBoxModel();
		if(album!=null)
		{
			this.genreModel.setSelectedItem(album.getGenre());
			this.tracksTable=new DaexTable(album);
			spinModel=new SpinnerNumberModel(album.getDvdTitle(), 1, album.getDvd().getTitles(), 1);
		}
		else
		{
			this.tracksTable=new DaexTable();
			spinModel=new SpinnerNumberModel(1, 1, 1, 0);
		}
		tracksTable.getModel().addTableModelListener(this);
		Program[] programs=new Program[3];
		programs[0]=Program.getInstance(Program.wav);
		programs[1]=Program.getInstance(Program.mp3);
		programs[2]=Program.getInstance(Program.ogg);
		encoders=new Encoder[3];
		encoders[0]=new DefaultEncoder(programs[0], DEFAULTQUALITY, false, "Wave");
		encoders[1]=new DefaultEncoder(programs[1], DEFAULTQUALITY, true, "Mp3");
		encoders[2]=new DefaultEncoder(programs[2], DEFAULTQUALITY, true, "OggVorbis");
		listModels=new DefaultSortedListModel[3];
		listModels[0]=new DefaultSortedListModel(encoders[0]);
		listModels[1]=new DefaultSortedListModel(encoders[1]);
		listModels[2]=new DefaultSortedListModel(encoders[2]);
		progressModel=new DaexProgressTableModel();
		DaexLogger.getDaexLogger().info("Init dei modelli corretto");
	}
	
	private void initAlbumInfo()
	{
		genreModel.setSelectedItem(album.getGenre());
		mainFrame.addAlbumInfo(album.getAlbum(), album.getArtist(), album.getDvdTitle());
		property.firePropertyChange(MODIFYANY, false, true);
	}
	
	private ArrayList<Song> trimmed(ArrayList<Song> mixed, Encoder encoder)
	{
		ArrayList<Song> songs=new ArrayList<Song>();
		try
		{
			for(Song s: mixed)
			{
				int n=album.getRecurrency(s.getTitle());
				if(n>0)
				{
					Song newS=(Song) s.clone();
					newS.setEndChapter(newS.getEndChapter()+(n-1));
					newS.setEncoder(encoder);
					songs.add(newS);
				}
				else
				{
					DaexLogger.getDaexLogger().log(Level.SEVERE, "Si sta registrando un oggetto Song che non esiste nell'Album", s);
				}
			}
		}
		catch (CloneNotSupportedException e)
		{
			// ignored, I wrote that class :-D
		}
		return songs;
	}
	
	/**
	 * Metodo di avvio dell'interfaccia grafica dell'applicativo.
	 */
	public void show()
	{
		mainFrame.setVisible(true);
	}
	
	/**
	 * Metodo per settare l'album in un secondo momento (ad esempio
	 * alla pressione delle rispettive voci nella gui).
	 * 
	 * @throws DvdException eccezione lanciata in caso di problemi di caricamento
	 */
	public void setAlbum() throws DvdException
	{
		try
		{
			this.album=getAlbumFromDevice(DaexProperties.getProperty("device"));
			this.tracksTable.setAlbum(album);
			initAlbumInfo();
		}
		catch (DvdException e)
		{
			DaexLogger.getDaexLogger().log(Level.WARNING, "Impossibile caricare l'album da "+DaexProperties.getProperty("device"), e);
			throw new DvdException(e);
		}
	}
	
	/**
	 * Metodo per settare l'album in un secondo momento (ad esempio
	 * alla pressione delle rispettive voci nella gui).
	 * 
	 * @param device il device da cui leggere
	 * @throws DvdException eccezione lanciata in caso di problemi di caricamento
	 */
	public void setAlbum(String device) throws DvdException
	{
		try
		{
			this.album=getAlbumFromDevice(device);
			this.tracksTable.setAlbum(album);
			initAlbumInfo();
		}
		catch (DvdException e)
		{
			DaexLogger.getDaexLogger().log(Level.WARNING, "Impossibile caricare l'album da "+device, e);
			throw new DvdException(e);
		}
	}
	
	/**
	 * Metodo per settare l'album in un secondo momento (ad esempio
	 * alla pressione delle rispettive voci nella gui).
	 * 
	 * @param filename il file da cui caricare l'oggetto
	 * @param device il device da cui caricare il dvd
	 * @throws DvdException eccezione lanciata in caso di problemi di caricamento
	 */
	public void setAlbum(File filename, String device) throws DvdException
	{
		try
		{
			DefaultAlbumWriter writer=new DefaultAlbumWriter(filename);
			this.album=writer.load(device);
			this.tracksTable.setAlbum(album);
			initAlbumInfo();
		}
		catch (Exception e)
		{
			DaexLogger.getDaexLogger().log(Level.WARNING, "Impossibile caricare l'album da "+filename+" e "+device, e);
			throw new DvdException(e);
		}
	}

	/**
	 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
	 */
	public void actionPerformed(ActionEvent e)
	{
		if(e.getSource() instanceof JComboBox)
		{
			if(album!=null)
			{
				// non ritorna null poiche' io costruisco il modello correttamente
				Genre g=(Genre) Genre.getGenreByName((String) genreModel.getSelectedItem());
				album.setGenre(g);
			}
		}
		else
		{
			if(album!=null)
			{
				String newArtist=((JTextField) e.getSource()).getText();
				album.setArtist(newArtist);
				tracksTable.refresh();
			}
		}
		property.firePropertyChange(MODIFYANY, false, true);
	}

	/**
	 * Aggiorna tutte le liste laterali a fronte di un aggiornamento
	 * della tabella cui e' agganciato.
	 * 
	 * @see javax.swing.event.TableModelListener#tableChanged(javax.swing.event.TableModelEvent)
	 */
	public void tableChanged(TableModelEvent e)
	{
		for(SortedListModel m: listModels)
		{
			m.refresh();
		}
		property.firePropertyChange(MODIFYANY, false, true);
	}

	/**
	 * Reinizializza le informazioni di album.
	 */
	public void reinit()
	{
		if(album!=null)
		{
			album.setAlbum(Album.GENERICALBUM);
			album.setArtist(Album.GENERICARTIST);
			album.setGenre(Album.GENERICGENRE);
			for(int i=0; i<album.getSongs().size(); i++)
			{
				album.getSongs().get(i).setStartChapter(i+1);
				album.getSongs().get(i).setEndChapter(i+1);
				album.getSongs().get(i).setTableValue(i+1);
			}
			initAlbumInfo();
		}
		clearAll();
	}
	
	/**
	 * Metodo per pulire tutte le tabelle degli encoders.
	 */
	public void clearAll()
	{
		for(SortedListModel m: listModels)
			m.clear();
	}
	
	/**
	 * Metodo per la stampa della tabella delle tracce.
	 * 
	 * @throws PrinterException eccezione lanciata in caso di problemi di stampa 
	 */
	public void printTracks() throws PrinterException
	{
		tracksTable.print();
	}
	
	/**
	 * Metodo per il salvataggio dell'attuale Album.
	 * 
	 * @param filename il file in cui salvare lo stato
	 * @throws AlbumWriterException eccezione lanciata in caso di problemi
	 */
	public void save(String filename) throws AlbumWriterException
	{
		try
		{
			// aggiornamento campi di solito non aggiornati
			album.setAlbum(mainFrame.getAlbum());
			DefaultAlbumWriter writer=new DefaultAlbumWriter(filename);
			writer.save(album);
		}
		catch (AlbumWriterException e)
		{
			DaexLogger.getDaexLogger().log(Level.WARNING, "Impossibile salvare il file in "+filename, e);
			throw new AlbumWriterException(e);
		}
	}
	
	/**
	 * Metodo per settare un nuovo titolo al posto del
	 * precedente.
	 * 
	 * @param title il titolo da settare
	 * @throws DvdException eccezione lanciata in caso di problemi di lettura dal device
	 */
	public void changeTitle(int title) throws DvdException
	{
		try
		{
			album.setDvdTitle(title);
			tracksTable.refresh();
			clearAll();
		}
		catch (DvdException e)
		{
			DaexLogger.getDaexLogger().log(Level.SEVERE, "Impossibile settare il titolo "+title, e);
			throw new DvdException(e);
		}
	}
	
	/**
	 * Metodo per iniziare le operazioni di encoding.
	 * 
	 * @return true se l'encoding e' avviato, false altrimenti
	 * @throws ProgramNotFoundException eccezione lanciata in caso di problemi di esecuzione
	 * @throws IOException eccezione lanciata in caso di problemi di creazione
	 */
	public boolean startEncode() throws ProgramNotFoundException, IOException
	{
		// sistemazione titolo album
		album.setAlbum(mainFrame.getAlbum());
		// settings dialog
		int total=0;
		for(SortedListModel l: listModels)
			total+=l.getElements().size();
		settingsDialog=new SettingsDialog(total);
		int result=JOptionPane.showConfirmDialog(mainFrame, settingsDialog, "Settaggi degli encoders", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.INFORMATION_MESSAGE, new ImageIcon());
		if(result!=JOptionPane.YES_OPTION)
		{
			return false;
		}
		// preparazione nuova lista Song
		ArrayList<Song> songs=new ArrayList<Song>();
		for(SortedListModel m: listModels)
			songs.addAll(trimmed(m.getElements(), m.getEncoder()));
		// settaggio encoders
		Song[] songsArray=new Song[songs.size()];
		songsArray=songs.toArray(songsArray);
		String outdir=settingsDialog.getOutDir()+File.separator+album.getArtist()+File.separator+album.getAlbum();
		encodingList=new DefaultEncodingList(songsArray, album.getDvd(), album.getDvdTitle(), outdir, settingsDialog.getTempDir());
		// preparazione lato grafica e runnable
		String[] titles=new String[songsArray.length];
		for(int i=0; i<titles.length; i++)
		{
			titles[i]=songsArray[i].getArtist()+" - "+songsArray[i].getTitle();
		}
		progressModel.setList(titles);
		executor=new DefaultEncoderExecutor(encodingList);
		executor.addPropertyChangeListener(EncoderExecutor.EXECPROPERTY, mainFrame);
		executor.addPropertyChangeListener(EncoderExecutor.PROGRESSION, new ProgressBarListener(mainFrame.getProgressBar()));
		executor.addPropertyChangeListener(EncoderExecutor.EXECOUTPUT, new TextAreaListener(ConsoleDialog.getConsole().getDocument()));
		executor.addPropertyChangeListener(EncoderExecutor.PARTIALOP, progressModel);
		// showtime!
		executor.encode();
		return true;
	}
	
	/**
	 * Metodo per visualizzare la console (se le operazioni di
	 * encoding sono iniziate).
	 */
	public void showConsole()
	{
		JOptionPane pane=new JOptionPane(ConsoleDialog.getConsole(), JOptionPane.INFORMATION_MESSAGE, JOptionPane.DEFAULT_OPTION, new ImageIcon());
		JDialog dialog=pane.createDialog(mainFrame, "Console output");
		dialog.setSize(400, 250);
		dialog.setVisible(true);
	}
	
	/**
	 * Metodo per fermare l'encoding.
	 */
	public void stopEncode()
	{
		if(executor!=null)
		{
			executor.interrupt();
		}
	}
	
	/**
	 * Controllo se vi e' un'operazione di encoding in corso
	 * o meno.
	 * 
	 * @return true se si sta codificando, false altrimenti
	 */
	public boolean checkRun()
	{
		return ( executor != null && executor.isEncoding() ) ? true : false;
	}
	
}
